package org.vaadin.nf4v.core.config.process;

import org.vaadin.nf4v.core.config.*;
import org.vaadin.nf4v.core.config.process.parser.ConfigParser;
import org.vaadin.nf4v.core.config.process.parser.ContextConfigParser;
import org.vaadin.nf4v.core.config.process.parser.NavigationConfigParser;
import org.vaadin.nf4v.core.config.process.parser.PageConfigParser;
import org.vaadin.nf4v.core.handler.Handler;
import org.vaadin.nf4v.core.handler.NavigationUncaughtErrorHandler;
import org.vaadin.nf4v.core.handler.UriHandler;
import org.vaadin.nf4v.core.handler.impl.*;

import org.vaadin.nf4v.ext.config.afternav.AfterNavigationAnnotationParser;
import org.vaadin.nf4v.ext.config.beforenav.BeforeNavigationAnnotationParser;
import org.vaadin.nf4v.ext.config.handler.OverrideHandlerAnnotationParser;
import org.vaadin.nf4v.ext.config.menu.MenuAnnotationParser;
import org.vaadin.nf4v.ext.config.param.ParamAnnotationParser;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * Base class for configuration dsl implementations.
 * Provides support for
 * 1. configuration parsers
 * 2. configuration phase lifecycle
 * <p/>
 * Configuration parser is extension point for configuration process.
 * It allows to add annotations support or any other configuration processing.
 * There are three types of configuration parsers:
 * navigation configuration, context configuration and page configuration parsers.
 * <p/>
 * Configurations are executed after dsl parsing in the followig order:
 * 1. page configuration parsers
 * 2. context configuration parsers
 * 3. navigation configuration parsers
 * <p/>
 * Note: do not add new configuration objects inside parsers, cause such configuration objects
 * may not be postprocessed by configuration processors and entire configuration may become inconsistent.
 * If for example, classpath scanning is required instead of dsl configuration, implement scanning in
 * configure().
 * <p/>
 * Processor lifecycle:
 * - register default parsers
 * - call configure() to build configuration
 * - execute parsers on configuration
 * - validate configuration
 */
public abstract class NavigationConfigProcessor {

    private List<NavigationConfigParser> navigationParsers = new ArrayList<NavigationConfigParser>();
    private List<ContextConfigParser> contextParsers = new ArrayList<ContextConfigParser>();
    private List<PageConfigParser> pageParsers = new ArrayList<PageConfigParser>();
    // resulted navigation config (which is filled by this processor)
    private NavigationConfig navigationConfig = new NavigationConfig();

    //todo parameters dictionary for parameter names validation?

    // todo helper methods to print config state (to help debugging configuration)

    protected NavigationConfigProcessor() {
        initDefaultExtensions();
        configure();
        processParsers();
        validate();
    }

    // detects parser type by interface(s)

    /**
     * Default configuration extensions initialization.
     * Note usually extensions are separated into two parts: configuration and runtime.
     * Runtime parts are registered in NavigableApplication#initDefaultExtensions()
     *
     * Override method to change default extensions. 
     */
    protected void initDefaultExtensions() {
        addConfigParsers(
                // @Override annotation support
                new OverrideHandlerAnnotationParser(),
                // @Param annotation support
                new ParamAnnotationParser(),
                // @AfterNavigation annotation support
                new AfterNavigationAnnotationParser(),
                // @BeforeNavigation annotation support
                new BeforeNavigationAnnotationParser(),
                // @Menu annotation support 
                new MenuAnnotationParser()
        );
    }

    /**
     * Registers configuration parsers.
     * Type of parser will be detected by implemented interface(s)
     *
     * @param parsers parsers to register
     */
    protected void addConfigParsers(ConfigParser... parsers) {
        for (ConfigParser parser : parsers) {
            if (parser instanceof NavigationConfigParser) {
                navigationParsers.add((NavigationConfigParser) parser);
            }
            if (parser instanceof ContextConfigParser) {
                contextParsers.add((ContextConfigParser) parser);
            }
            if (parser instanceof PageConfigParser) {
                pageParsers.add((PageConfigParser) parser);
            }
        }
    }

    /**
     * Execute parsers on configuration objects, created inside configure() method (after dsl processing)
     */
    private void processParsers() {
        for (ContextConfig contextConfig : navigationConfig.getContextConfigs()) {
            for (PageConfig pageConfig : contextConfig.getPages()) {
                for (PageConfigParser parser : pageParsers) {
                    parser.parse(this, contextConfig, pageConfig);
                }
            }
            for (ContextConfigParser parser : contextParsers) {
                parser.parse(this, contextConfig);
            }
        }
        for (NavigationConfigParser parser : navigationParsers) {
            parser.parse(this);
        }
    }

    /**
     * Configure your navigation here using provided DSL.
     * It is better to follow this structure:
     * 1. Register parsers (if any)
     * 2. Register all contexts, using context()
     * 3. Register pages shared between contexts (if you have more than one context), using inContexts()
     * 4. Register pages specific to context, using inContext()
     */
    protected abstract void configure();

    /**
     * Validates context correctness after configure() and parsers processing.
     *
     * @throws ContextConfigurationException on errors in configuration
     */
    private void validate() throws ContextConfigurationException {
        if (getNavigationConfig().getContextConfigs().size()==0){
            throw new ContextConfigurationException("Configuration is empty");
        }

        boolean foundMain = false;
        List<String> contextNames = new LinkedList<String>();
        for(ContextConfig config:getNavigationConfig().getContextConfigs()){
            // context name validation
            String context = config.getName();
            if (context==null){
                throw new ContextConfigurationException("No context name set for "+config);
            }
            context = context.trim();
            config.setName(context);
            if (contextNames.contains(context)){
                throw new ContextConfigurationException("Duplicate context name '"+context+"' in "+config);
            }
            contextNames.add(context);

            // window class validation
            if (config.getWindowClass()==null){
                throw new ContextConfigurationException("No window class set for "+config);
            }

            if (config.isMainWindow()){
                if (foundMain){
                    throw new ContextConfigurationException("Duplicate main window declaration in "+config);
                }
                foundMain = true;
            }

            // pages validation
            if (config.getPages().size()==0){
                throw new ContextConfigurationException("No pages set for context "+context+": "+config);
            }

            List<String> pageUris = new LinkedList<String>();
            List<Class> pageClasses = new LinkedList<Class>();
            boolean foundHomePage = false;
            for (PageConfig pageConfig: config.getPages()){
                // check uri
                String uri = pageConfig.getUri();
                if (uri!=null){
                    uri = uri.trim();
                    if ("".equals(uri)){
                        pageConfig.setUri(null);
                    } else {
                        if (pageUris.contains(uri)){
                            throw new ContextConfigurationException("Duplicate page uri '"+uri+"' in context '"+context+"' in "+pageConfig);
                        }
                        pageUris.add(uri);
                    }
                }

                if (pageConfig.getPageClass()==null){
                    throw new ContextConfigurationException("No page class in context '"+context+"' set for "+pageConfig);
                }
                if (pageClasses.contains(pageConfig.getClass())){
                    throw new ContextConfigurationException("Duplicate page class '"+pageConfig.getPageClass()+"' in context '"+
                            context+"' in "+pageConfig);
                }
                pageClasses.add(pageConfig.getPageClass());

                if (pageConfig.isHomePage()){
                    if (foundHomePage){
                        throw new ContextConfigurationException("Duplicate home page declaration in context '"+context+"' in "+pageConfig);
                    }
                    foundHomePage = true;
                }
            }
            // set home page
            if (!foundHomePage){
                config.getPages().get(0).setHomePage(true);
            }
        }

        // set main context
        if (!foundMain) {
            getNavigationConfig().getContextConfigs().get(0).setMainWindow(true);
        }

        // setting default handlers
        if (navigationConfig.getHandler(UriHandler.class) == null) {
            registerHandler(navigationConfig, new DefaultUriHandler("/"));
        }
        if (navigationConfig.getHandler(NavigationUncaughtErrorHandler.class) == null) {
            registerHandler(navigationConfig, new DefaultNavigationUncaughtErrorHandler());
        }
        if (navigationConfig.getHandler(DefaultParamResolver.class) == null) {
            registerHandler(navigationConfig, new DefaultParamResolver("/"));
        }
        if (navigationConfig.getHandler(DefaultInvalidUriHandler.class) == null) {
            registerHandler(navigationConfig, new DefaultInvalidUriHandler());
        }
        if (navigationConfig.getHandler(DefaultHomePageResolver.class) == null) {
            registerHandler(navigationConfig, new DefaultHomePageResolver());
        }
        if (navigationConfig.getHandler(DefaultWindowFactory.class) == null) {
            registerHandler(navigationConfig, new DefaultWindowFactory());
        }
        if (navigationConfig.getHandler(DefaultPageFactory.class) == null) {
            registerHandler(navigationConfig, new DefaultPageFactory());
        }
    }

    // todo handler registration with different handler class (to allow subclass registration)

    /**
     * Registers handler for provided config.
     * Instantiates handler if it wasn't instantiated yet.
     *
     * @param config  configuration to register handler for
     * @param handler handler implementation class
     * @throws ContextConfigurationException if handler didn't implement handler interface or if
     *                                       handler is already registered for this configuration.
     */
    @SuppressWarnings("unchecked")
    public <T extends Handler> void registerHandler(AbstractConfig config, Class<T> handler) throws ContextConfigurationException {
        // searching for base handler interface
        Class<Handler> handlerClass = null;
        for (Class clazz : handler.getClass().getInterfaces()) {
            // searching for interface which extends Handler
            for (Class clazz2 : clazz.getInterfaces()) {
                if (clazz2 == Handler.class) {
                    handlerClass = clazz;
                    break;
                }
            }
        }
        if (handlerClass == null) {
            throw new ContextConfigurationException("Can't find base interface of handler " + handler.getClass().getName());
        }
        if (config.getAttribute(handlerClass.getName()) != null) {
            throw new ContextConfigurationException("Implementation for handler " + handlerClass.getName() + " is already registered: " + config.getAttribute(handlerClass.getName()));
        }
        config.setAttribute(handlerClass.getName(), handler.getClass());
        // adding instance if no instance already registered
        if (navigationConfig.getHandlerInstance(handlerClass) == null) {
            navigationConfig.setHandlerInstance(handler, instantiateHandler(handler));
        }
    }

    /**
     * Instantiates handler.
     * May be overridden to customize handler instantiation logic.
     *
     * @param handlerClass handler implementation class
     * @return handler instance
     * @throws ContextConfigurationException if instantiation failed
     */
    protected <T extends Handler> T instantiateHandler(Class<T> handlerClass) throws ContextConfigurationException {
        try {
            return handlerClass.newInstance();
        } catch (Exception e) {
            throw new ContextConfigurationException("Problem while instantiating handler of class " + handlerClass + ". Handler should have no-arg constructor", e);
        }
    }

    /**
     * Registers handler implementation for configuration using handler instance.
     * Current limitation: it is not possible to register handler instances of the same implementation class
     * for different configurations (to use handlers of the same class but differently configured)
     *
     * @param config  configuration
     * @param handler handler instance
     * @throws ContextConfigurationException if handler didn't implement handler interface or if
     *                                       handler is already registered for this configuration.
     */
    @SuppressWarnings("unchecked")
    public <T extends Handler> void registerHandler(AbstractConfig config, T handler) throws ContextConfigurationException {
        // todo remove duplication
        // searching for base handler interface
        Class<Handler> handlerClass = null;
        for (Class clazz : handler.getClass().getInterfaces()) {
            // searching for interface which extends Handler
            for (Class clazz2 : clazz.getInterfaces()) {
                if (clazz2 == Handler.class) {
                    handlerClass = clazz;
                    break;
                }
            }
        }
        if (handlerClass == null) {
            throw new ContextConfigurationException("Can't find base interface of handler " + handler.getClass().getName());
        }
        if (config.getAttribute(handlerClass.getName()) != null) {
            throw new ContextConfigurationException("Implementation for handler " + handlerClass.getName() + " is already registered: " + config.getAttribute(handlerClass.getName()));
        }
        config.setAttribute(handlerClass.getName(), handler.getClass());
        navigationConfig.setHandlerInstance((Class<Handler>) handler.getClass(), handler);
    }

    /**
     * Note: be aware that this configuration may be incomplete if called during configuration phase
     *
     * @return navigation configuration.
     */
    public NavigationConfig getNavigationConfig() {
        return navigationConfig;
    }
}
